﻿using System;
using System.Reflection;
using System.Runtime.InteropServices;

using OpenTK.Graphics.OpenGL;
using OpenTK;

namespace GLSL
{
    /// <summary>
    /// Inputs a array of vertices into
    /// a grphics cards memory for fast accsess
    /// </summary>
    class VertexBuffer
    {
        public readonly int vboID;
        private object vertices;
        private BeginMode drawMode;
        private PropertyInfo[] vertexStruct;
        private BufferUsageHint bufferUsage;
        private int vertexStride;
        private int dataLength;

        /// <summary>
        /// initializes the class and sets
        /// a new Buffer ID
        /// </summary>
        public VertexBuffer(BufferUsageHint bufferUsage)
        {
            GL.GenBuffers(1, out vboID);
            drawMode =  BeginMode.Quads;
            this.bufferUsage = bufferUsage;
        }

        /// <summary>
        /// Set what draw mode the VBO shout run at
        /// </summary>
        /// <param name="drawMode">BeginMode</param>
        public void setDrawMode(BeginMode drawMode)
        {
            this.drawMode = drawMode;
        }

        /// <summary>
        /// Uploads Vertices to the graphics card
        /// </summary>
        /// <typeparam name="T">Vertex Struct</typeparam>
        /// <param name="vertices">The array of vertices</param>
        /// <param name="vboType">what buffer mode the VBO is.</param>
        public void SetData<T>(T[] vertices) where T : struct
        {
            this.vertices = vertices;

            vertexStruct = vertices[0].GetType().GetProperties();
            vertexStride = Marshal.SizeOf(typeof(T));
            dataLength = vertices.Length;

            GL.BindBuffer(BufferTarget.ArrayBuffer, vboID);
            GL.BufferData(BufferTarget.ArrayBuffer, new IntPtr(dataLength * vertexStride), vertices, bufferUsage);
            GL.BindBuffer(BufferTarget.ArrayBuffer, 0);

            CheckForError();
        }

        /// <summary>
        /// Called when we need to draw what we
        /// have in the memory to the screen
        /// </summary>
        public void Render()
        {
            int pointer = 0;

            GL.BindBuffer(BufferTarget.ArrayBuffer, this.vboID);

            //Set up each of ht epointers based on
            //What is the Struct made up of
            foreach (var attribute in vertexStruct)
            {
                if (attribute.Name == "Postion")
                {
                    GL.EnableClientState(ArrayCap.VertexArray);
                    GL.VertexPointer(3, VertexPointerType.Float, vertexStride, new IntPtr(pointer));
                    pointer += Vector3.SizeInBytes;
                }
                else if (attribute.Name == "Normal")
                {
                    GL.EnableClientState(ArrayCap.NormalArray);
                    GL.NormalPointer(NormalPointerType.Float, vertexStride, new IntPtr(pointer));
                    pointer += Vector3.SizeInBytes;
                }
                else if (attribute.Name == "TextureUV")
                {
                    GL.EnableClientState(ArrayCap.TextureCoordArray);
                    GL.TexCoordPointer(2, TexCoordPointerType.Float, vertexStride, new IntPtr(pointer));
                    pointer += Vector2.SizeInBytes;
                }
                else if (attribute.Name == "Color")
                {
                    GL.EnableClientState(ArrayCap.ColorArray);
                    GL.ColorPointer(4, ColorPointerType.Float, vertexStride, new IntPtr(pointer));
                    pointer += Vector4.SizeInBytes;
                }
            }

            GL.DrawArrays(drawMode, 0, dataLength);

            foreach (var attribute in vertexStruct)
            {
                if (attribute.Name == "Postion")
                {
                    GL.DisableClientState(ArrayCap.VertexArray);
                }
                else if (attribute.Name == "Normal")
                {
                    GL.DisableClientState(ArrayCap.NormalArray);
                }
                else if (attribute.Name == "TextureUV")
                {
                    GL.DisableClientState(ArrayCap.TextureCoordArray);
                }
                else if (attribute.Name == "Colour")
                {
                    GL.DisableClientState(ArrayCap.ColorArray);
                }
            }

            GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
            CheckForError();
        }

        /// <summary>
        /// Runs when we the need the VBO to be destoryed
        /// </summary>
        public void Dispose()
        {
            if (this.vertices != null)
            {
                int vboID = this.vboID;
                GL.DeleteBuffers(1, ref vboID);
            }

            this.vertices = null;
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Queries OpenGL for any errors and 
        /// throws an exception if there is
        /// </summary>
        private void CheckForError()
        {
            ErrorCode error = GL.GetError();

            if (error != ErrorCode.NoError)
            {
                throw new Exception("Vertex Buffer Error:" + Environment.NewLine + error.ToString());
            }
        }
    }
}
